Add reflect and symmetric padding modes to mx.pad - #3608
Conversation
63a3e46 to
579bfd3
Compare
|
Thanks @zcbenz! I went through #2862 carefully — functionally the two cover the same ground ( Differences I found:
Happy to adjust naming or docs to whatever you prefer. |
|
Following up now that #2862 has been closed — that leaves this PR as the remaining path for |
71a68c6 to
f38f452
Compare
|
Rebased onto current main — this had drifted into a conflict, so it wasn't mergeable regardless of review. It's clean now at The only conflict was documentation: main renamed the documented argument For the record on the earlier hold: #2862 was closed unmerged on July 1, so this is the remaining path for The new head has no checks on it yet — fork runs need an approval to start, if someone can grant it. |
Implements numpy.pad-compatible "reflect" and "symmetric" modes for mx.pad, matching numpy semantics for arbitrary pad sizes (the reflection repeats when the pad width exceeds the axis length). - mlx/ops.cpp: reflect_pad helper builds a per-axis triangle-wave index map and gathers with take; one take per padded axis. reflect uses period 2(n-1) and skips the edge; symmetric uses period 2n and repeats the edge. n==1 maps to 0. - python/src/ops.cpp: extend the pad mode Literal and docstring. - python/tests/test_ops.py: test_pad_reflect_symmetric covers in-bounds, multi-reflect, asymmetric per-axis, zero-width sides, and degenerate axes (n==1, n==2), checked against numpy.pad. - tests/ops_tests.cpp: reflect/symmetric CHECK cases incl. multi-reflect.
f38f452 to
f2ef917
Compare
zcbenz
left a comment
There was a problem hiding this comment.
Sorry for the late response, I had to find someone to verify the correctness of the implementation before merging, since no one did I have to spend some time myself.
So this is not a performant implementation: it builds the indices with the same length of output in the host, which would be rather slow for large inputs.
A performant implementation should follow edge_pad: use slice_update to write paddings and unpadded array to output, with host calculating only the start/stop indices. The reflect_pad only needs to change how the paddings are generated, by slicing and then flipping, you can find some idea in JAX's implementation https://github.com/jax-ml/jax/blob/8d1be7d7d1c4bd19a0de12c917709dd20f2de34d/jax/_src/numpy/lax_numpy.py#L3988.
Follows edge_pad's pattern: place the input into a zero-filled output with slice_update, then extend each axis outward via slice+flip instead of building a host-side index array of output length and gathering. A pad width larger than the axis loops in tiles, re-slicing the data just written to continue the periodic reflect/symmetric pattern (matches numpy.pad, including multi-reflect). Addresses zcbenz's review on ml-explore#3608: sub-ms on 5M/4M-element arrays for the common in-bounds case, vs. an O(output size) host loop + gather before. All 154 test_ops.py cases pass, including the existing multi-reflect coverage that exercises the new tiling loop.
|
Rewrote it to follow Same structure as All 154 |
CI's clang-format hook flagged the two dispatcher lines as too long.
|
Fixed the clang-format failure from the last run — my two |
Summary
Adds
numpy.pad-compatible\"reflect\"and\"symmetric\"modes tomx.pad. These join the existing\"constant\"and\"edge\"modes.Both modes match
numpy.padsemantics for arbitrary pad sizes — when the pad width exceeds the axis length the reflection repeats, exactly as NumPy does. (Earlier attempts at these modes were limited topad < dim; this implementation removes that restriction.)reflect— mirror padding that does not repeat the edge value (period2(n-1)).symmetric— mirror padding that does repeat the edge value (period2n).Motivation
This was developed as part of porting mobileportrait-mlx (a TPS-based face-animation model) to pure MLX for training and inference on Apple Silicon. The full pipeline — CelebV-HQ frame extraction, keypoint cache, TPS warping, perceptual loss — trains end-to-end in MLX on an M3 Max. After training on 3,000 clips the model runs inference at 22.6 fps (256×256, dense-motion + inpainting stage, M3 Max) and reaches render L1 = 0.0566 on held-out clips.
Implementation
reflect_pad(inmlx/ops.cpp) builds a per-axis index map with a triangle-wave reflection function and gathers withtake— onetakeper padded axis. A degenerate axis of length 1 maps every coordinate to 0. No new primitive or kernel is introduced; it composes existing ops, so it works on every backend and is differentiable for free.Files changed
mlx/ops.cpp—reflect_padhelper +reflect/symmetricdispatch branches inpad.python/src/ops.cpp— extend themodeLiteraland docstring.python/tests/test_ops.py—test_pad_reflect_symmetric.tests/ops_tests.cpp— reflect/symmetricCHECKcases incl. multi-reflect.Testing
All run locally on an M3 Max:
test_pad_reflect_symmetric— 13 shape/pad-width cases × 2 modes compared element-for-element againstnumpy.pad(in-bounds, multi-reflect where pad ≫ axis, asymmetric per-axis, zero-width sides, and degenerate axesn==1,n==2). Exact match.tests/ops_tests.cpp"test pad" — 9 assertions pass.251 cases / 251 passed,3442 assertions / 0 failed.